// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: jhkim

#include <array>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <locale>

#include "cc/appcoin2/research/util/option_util.h"

using json = nlohmann::json;

constexpr double MANDATE_RF = 0.01;
constexpr double MANDATE_VOL = 1.5;

using Mandates = std::vector<std::array<double, 3>>;  // array {range, raw, adj}

template <class T>
std::string fmt_commas(T value) {
  std::ostringstream out;
  out << std::fixed << std::setprecision(0) << value;
  std::string str = out.str();
  int len = str.length();
  if (len > 3) {
    int pos = len - 3;
    while (pos > 0) {
      str.insert(pos, ",");
      pos -= 3;
    }
  }
  return str;
}

bool is_strike_fixed(const std::string& strike_price) {
  return std::all_of(strike_price.begin(), strike_price.end(), [](char c) {
    return std::isdigit(c) || c == '.';
  });
}

double get_atm_multiple(const std::string& strike_price) {
  const std::string atm_multiple = boost::replace_all_copy(strike_price, "ATM", "");
  return std::stod(atm_multiple);
}

std::vector<json> read_config(const std::string& config_file) {
  std::ifstream file(config_file);
  json config;
  file >> config;
  std::vector<json> configs;
  for (auto elem : config["configs"]) {
    configs.push_back(elem);
  }
  return configs;
}

Mandates get_mandate(
    const json& opt_configs,
    const double flow_alloc,
    const double adj_factor,
    double stock_price,
    const std::vector<double>& strike_prices,
    const bool is_pure_dh,
    const double mandate_price_rate,
    const std::vector<double>& mandate_ranges) {
  Mandates mandate_ranges_result{};

  if (!is_pure_dh) {  // provide 1% 3% mandate since it's liquid
    SPDLOG_INFO("Not pure delta-hedged state");
    double notional{0.0};
    for (const auto& opt_config : opt_configs) {
      if (stock_price == 0.0) {
        CHECK(opt_config.contains("notional") && !opt_config.contains("qty"))
            << "notional should be provided if S == 0.0";
        notional += opt_config["notional"].get<double>();
      } else {
        notional += opt_config["qty"].get<double>() * stock_price;
      }
    }
    const double flow_alloc_notional = flow_alloc * notional;
    const double raw_1percent = flow_alloc_notional * 10000.0 / 700000.0;
    const double raw_3percent = raw_1percent * 2.5;
    const double adj_1percent = raw_1percent / adj_factor;
    const double adj_3percent = raw_3percent / adj_factor;
    auto is_single_exchange = std::getenv("IS_SINGLE_EXCHANGE");
    if (is_single_exchange && std::string(is_single_exchange) == "1") {
      mandate_ranges_result.push_back({0.03, raw_1percent, adj_1percent});
      mandate_ranges_result.push_back({0.05, raw_3percent, adj_3percent});
    } else {
      mandate_ranges_result.push_back({0.01, raw_1percent, adj_1percent});
      mandate_ranges_result.push_back({0.03, raw_3percent, adj_3percent});
    }
  } else {
    SPDLOG_INFO("Pure delta-hedged state");
    stock_price *= mandate_price_rate;
    // Assume delta-hedged state and calculate only delta-hedge making orders
    const double rf = 0.01;
    const double volatility = 1.75;
    const int layer_num = 100;
    std::vector<double> ubs{}, lbs{};
    for (const double& mandate_range : mandate_ranges) {
      if (!ubs.empty())
        CHECK_GT(stock_price * (1.0 + mandate_range), ubs.back())
            << "mandate range should be ascending";
      ubs.push_back(stock_price * (1.0 + mandate_range));
      lbs.push_back(stock_price * (1.0 - mandate_range));
      mandate_ranges_result.push_back({mandate_range, 0.0, 0.0});
    }
    // calculate ask side
    double prev_pos{0.0};
    const double& max_price = ubs.back();
    for (int i = 0; i < layer_num + 1; ++i) {
      // linearspace price from stock_price to ub_3percent
      double cur_pos{0.0};
      const double price = stock_price + (max_price - stock_price) / layer_num * i;
      for (int j = 0; j < opt_configs.size(); ++j) {
        const auto& opt_config = opt_configs[j];
        const auto strike_price = strike_prices[j];
        const double T = opt_config["time_to_maturity"].get<double>();
        const double delta = call_delta(price, strike_price, rf, T, volatility);
        const double qty = opt_config["qty"].get<double>();
        cur_pos += -delta * qty;
      }
      if (i != 0) {
        const double mand = (prev_pos - cur_pos) * price;
        CHECK_GT(mand, 0.0) << fmt::format(
            "mand: {} prev_pos: {} cur_pos: {} price: {}",
            mand,
            prev_pos,
            cur_pos,
            price);
        for (int j = 0; j < ubs.size(); ++j) {
          const double& ub = ubs[j];
          if (price <= ub) {
            mandate_ranges_result[j][1] += mand;
            mandate_ranges_result[j][2] += mand;  // no adjust_factor
          }
        }
      }
      prev_pos = cur_pos;
    }
    // calculate bid side
    prev_pos = 0.0;
    const double& min_price = lbs.back();
    for (int i = 0; i < layer_num + 1; ++i) {
      // linearspace price from stock_price to lb_3percent
      double cur_pos{0.0};
      const double price = stock_price - (stock_price - min_price) / layer_num * i;
      for (int j = 0; j < opt_configs.size(); ++j) {
        const auto& opt_config = opt_configs[j];
        const auto strike_price = strike_prices[j];
        const double T = opt_config["time_to_maturity"].get<double>();
        const double delta = call_delta(price, strike_price, rf, T, volatility);
        const double qty = opt_config["qty"].get<double>();
        cur_pos += -delta * qty;
      }
      if (i != 0) {
        const double mand = (cur_pos - prev_pos) * price;
        CHECK_GT(mand, 0.0) << fmt::format(
            "mand: {} prev_pos: {} cur_pos: {} price: {}",
            mand,
            prev_pos,
            cur_pos,
            price);
        for (int j = 0; j < lbs.size(); ++j) {
          const double& lb = lbs[j];
          if (price >= lb) {
            mandate_ranges_result[j][1] += mand;
            mandate_ranges_result[j][2] += mand;  // no adjust_factor
          }
        }
      }
      prev_pos = cur_pos;
    }
  }
  return mandate_ranges_result;
}

int main(int argc, char* argv[]) {
  InitializeTracer();
  cxxopts::Options opt("option_calculator", "options calculator");
  opt.add_options()("config", "", cxxopts::value<std::string>());
  opt.add_options()("is_pure_dh", "", cxxopts::value<bool>());
  auto res = opt.parse(argc, argv);
  CHECK(res.count("config"));
  CHECK(res.count("is_pure_dh"));
  std::ifstream file(res["config"].as<std::string>());
  const bool is_pure_dh = res["is_pure_dh"].as<bool>();
  json config;
  file >> config;
  SPDLOG_INFO("{}", config.dump(2));
  if (is_pure_dh)
    SPDLOG_INFO("Pure delta-hedged state");
  else
    SPDLOG_INFO("Not pure delta-hedged state");
  const std::string& project_name = config.at("project_name");
  const std::string& symbol = config.at("symbol");

  const auto& pricing_configs = config["pricing_configs"];
  const auto& opt_configs = config["option_configs"];
  const double adj_factor = config["adj_factor"].get<double>();
  const double flow_alloc = config["flow_allocation"].get<double>();
  const double mandate_price_rate = config["mandate_price_rate"].get<double>();
  std::vector<double> mandate_ranges{};
  for (const auto& mandate_range : config["mandate_ranges"]) {
    mandate_ranges.push_back(mandate_range.get<double>());
  }
  const double S = pricing_configs["stock_price"].get<double>();
  const bool is_notional_mode = S == 0.0;
  const double sigma = pricing_configs["volatility"].get<double>();
  CHECK_EQ(sigma, MANDATE_VOL) << "sigma should be 1.5";
  const int64_t& num_simulations = pricing_configs.at("num_simulations");
  std::stringstream ss1, ss2, ss3;
  ss1 << "\n# of Iteration: " << fmt_commas(num_simulations) << "\n";
  ss2 << "\n";
  std::vector<double> strike_prices;
  for (const auto& r : pricing_configs["risk_free_rate"]) {
    double opt_value = 0.0;
    const double rf = r.get<double>();
    std::string tmp_str;
    if (is_notional_mode) {
      tmp_str = fmt::format(
        "[Pricing Config] risk_free_rate: {}%\tvolatility: {}%",
        100.0 * rf,
        100.0 * sigma);
    } else {
      tmp_str = fmt::format(
        "[Pricing Config] risk_free_rate: {}%\tvolatility: {}%\tstock_price: {}\n",
        100.0 * rf,
        100.0 * sigma,
        S);
    }
    SPDLOG_INFO(tmp_str);
    ss1 << tmp_str;
    ss2 << tmp_str;
    for (const auto& opt_config : opt_configs) {
      double qty{0.0};
      const double T = opt_config["time_to_maturity"].get<double>();
      const std::string& strike_price = opt_config.at("strike_price");
      double call_price{0.0};
      double K{0.0};
      std::string strike_info{strike_price};
      if (is_strike_fixed(strike_price)) {
        K = std::stod(strike_price);
        call_price = option_price_mc(S, K, rf, T, sigma, num_simulations);
      } else {
        const int avg_price_days = opt_config["avg_price_days"].get<int>();
        const double avg_price_start = opt_config["avg_price_start"].get<double>();
        const double atm_multiple = get_atm_multiple(strike_price);
        CHECK_GT(T, avg_price_start)
            << "time to maturity should be greater than average price start";
        CHECK_GE(T, avg_price_start + avg_price_days / 365.0)
            << "time to maturity should be greater than average price end";
        if (is_notional_mode) {
          CHECK(opt_config.contains("notional") && !opt_config.contains("qty"))
              << "notional should be provided if S == 0.0";
          const double notional = opt_config["notional"].get<double>();
          qty = 100.0;
          const double tmp_S = notional / qty;
          std::tie(call_price, K) = option_price_mc_step(
              tmp_S,
              avg_price_days,
              avg_price_start,
              atm_multiple,
              rf,
              T,
              sigma,
              num_simulations);
          K *= qty;
          strike_info = fmt::format(
              "{}(Avg Days: {}d, Avg Start Date: {}Y), Simulated Strike As Notional: {}",
              strike_price,
              avg_price_days,
              avg_price_start,
              fmt_commas(K));
        } else {
          qty = opt_config["qty"].get<double>();
          std::tie(call_price, K) = option_price_mc_step(
              S,
              avg_price_days,
              avg_price_start,
              atm_multiple,
              rf,
              T,
              sigma,
              num_simulations);
          strike_info = fmt::format(
              "{}(Avg Days: {}d, Avg Start Date: {}Y), Simulated Strike: {:.8f}",
              strike_price,
              avg_price_days,
              avg_price_start,
              K);
        }
      }
      opt_value += call_price * qty;
      std::string tmp;
      if (is_notional_mode) {
        tmp = fmt::format(
            "[Option Config] Strike Notional Info: {}\tReceived Notional: {}\tMaturity: {}Year\tOption Value: {}\n",
            strike_info,
            fmt_commas(opt_config["notional"].get<double>()),
            T,
            fmt_commas(call_price * qty));
      } else {
        tmp = fmt::format(
            "[Option Config] Strike: {}\tQTY: {}\tMaturity: {}Year\tOption Value: {}\n",
            strike_info,
            fmt_commas(qty),
            T,
            fmt_commas(call_price * qty));
      }
      SPDLOG_INFO(tmp);
      ss1 << tmp;
      if (rf == MANDATE_RF) strike_prices.push_back(K);
    }
    SPDLOG_INFO("Total Option Value: {}\n", fmt_commas(opt_value));
    ss1 << fmt::format("Total Option Value: {}\n\n", fmt_commas(opt_value));
    ss2 << fmt::format("Total Option Value: {}\n\n", fmt_commas(opt_value));
  }
  CHECK_EQ(strike_prices.size(), opt_configs.size());
  const auto mandates = get_mandate(
      opt_configs,
      flow_alloc,
      adj_factor,
      S,
      strike_prices,
      is_pure_dh,
      mandate_price_rate,
      mandate_ranges);
  ss3 << fmt::format(
      "[Mandate] FLOW Alloc: {}%\tAdj Factor: {}%\n[Mandate] ",
      flow_alloc * 100.0,
      adj_factor * 100.0);
  for (const auto& mandate : mandates) {
    ss3 << fmt::format(
        "Raw {}%: {}\tAdj {}%: {} / ",
        mandate[0] * 100.0,
        fmt_commas(mandate[1]),
        mandate[0] * 100.0,
        fmt_commas(mandate[2]));
  }
  ss3 << "\n";

  const std::string report = fmt::format(
      "\n##########################################################################################"
      "##"
      "########\n{}({}) Pricing "
      "Result\n{}{}{}################################################################"
      "####################################",
      project_name,
      symbol,
      ss1.str(),
      ss2.str(),
      ss3.str());
  SPDLOG_INFO(report);
  std::ofstream out(fmt::format(
      "{}/sim_result/{}_{}.txt",
      std::string(std::getenv("COIN_REPO")),
      project_name,
      symbol));
  out << report;

  return 0;
}
